////////////////////////////////////////////////////////////////////////////////
/// Midnight Circus 2007
////////////////////////////////////////////////////////////////////////////////

namespace GatherTogether
{
    /// <summary>
    /// The base class of exceptions that will be thrown by the parser when
    /// it encounters errors.
    /// </summary>
    class ParserException : System.ApplicationException
    {
        public ParserException(string message)
            : base(message)
        {
        }
    }

    class Parser
    {
        public void Parse(string filename, GatherDB gatherDB)
        {
            System.IO.StreamReader streamReader = new System.IO.StreamReader(filename);
            // TODO handle failure

            string dbString = streamReader.ReadToEnd();

            streamReader.Close();

            // HACK: assume this string is the first of the last of the miscellaneous setting information
            // HACK: doing this before the replace (below), so that tabs are preserved for the dump
            // TODO: handle failure
            gatherDB.Settings = dbString.Substring(dbString.IndexOf("Gatherer_SavedSettings_AccountWide"));

            dbString = dbString.Replace(" ", "");
            dbString = dbString.Replace("\t", "");
            dbString = dbString.Replace("\r", "");
            dbString = dbString.Replace("\n", "");

            string gatherItems = GetSection(dbString, "GatherItems");
            string gatherDrops = GetSection(dbString, "GatherDrops");
            string gatherDropRates = GetSection(dbString, "Gatherer_DropRates");

            ParseGatherItems(gatherDB.Items, gatherItems);

            if (gatherDrops != null)
            {
                ParseGatherDrops(gatherDB.Drops, gatherDrops);
            }

            if (gatherDropRates != null)
            {
                ParseGatherDropRates(gatherDB.DropRates, gatherDropRates);
            }
        }

        private void ThrowException(string message)
        {
            ParserException e = new ParserException(message);
            throw e;
        }

        private void ParseGatherItems(GatherItems.GatherItemsDB itemsDB, string gatherItems)
        {
            // ensure we are dealing with a database version that we understand
            CheckDatabaseVersion(ref gatherItems, "2");

            // Parse the continent list
            int continentIndex = 1;
            int currentIndex = 0;
            while (currentIndex < gatherItems.Length && continentIndex <= GatherConstants.NumberOfContinents)
            {
                int startBracket = gatherItems.IndexOf('[', currentIndex);
                int startBrace = gatherItems.IndexOf('{', currentIndex);
                if (startBracket == -1 && startBrace == -1)
                    ThrowException("Error parsing continent list");
                else if (startBrace == -1 || startBracket < startBrace)
                {
                    // Parse indexed continent
                    int endBracket = gatherItems.IndexOf(']', startBracket + 1);
                    continentIndex = System.Int32.Parse(gatherItems.Substring(startBracket + 1, endBracket - startBracket - 1));

                    // Eat the '=' between the index and the continent
                    if (gatherItems.IndexOf('=', endBracket + 1) != endBracket + 1)
                        ThrowException("Error parsing indexed continent");
                    else
                        endBracket++;

                    string continent = GetBlock(gatherItems, endBracket + 1);
                    ParseContinent(continent, itemsDB[continentIndex - 1]);
                    currentIndex = endBracket + continent.Length + 3;

                    // Eat the ',' following the continent
                    if (gatherItems.IndexOf(',', currentIndex) != currentIndex)
                        ThrowException("Error parsing continent list");
                    else
                        currentIndex++;
                    continentIndex++;
                }
                else
                {
                    // Parse unindexed continent
                    string continent = GetBlock(gatherItems, startBrace);
                    ParseContinent(continent, itemsDB[continentIndex - 1]);
                    currentIndex = startBrace + continent.Length + 2;

                    // Eat the ',' following the continent
                    if (gatherItems.IndexOf(',', currentIndex) != currentIndex)
                        ThrowException("Error parsing continent list");
                    else
                        currentIndex++;

                    // Check the comment with continent index matches 
                    if (ParseIndexComment(gatherItems, ref currentIndex) != continentIndex)
                        ThrowException("Warning continent index does not match it's commented index");
                    continentIndex++;
                }
            }
        }

        private int ParseIndexComment(string gatherItems, ref int currentIndex)
        {
            // TODO: Check that this is actually a comment
            int startBracket = gatherItems.IndexOf('[', currentIndex);
            if (startBracket == -1)
                ThrowException("Error parsing an index comment");
            int endBracket = gatherItems.IndexOf(']', startBracket + 1);
            int result = System.Int32.Parse(gatherItems.Substring(startBracket + 1, endBracket - startBracket - 1));
            currentIndex = endBracket + 1;
            return result;
        }

        private void ParseContinent(string continent, GatherItems.Continent currentContinent)
        {
            int currentIndex = 0;
            while (currentIndex < continent.Length)
            {
                // Parse the name of the area
                int startBracket = continent.IndexOf('[', currentIndex);
                if (startBracket == -1)
                    ThrowException("Error parsing area list");
                int endBracket = continent.IndexOf(']', startBracket + 1);
                string areaName = continent.Substring(startBracket + 2, endBracket - startBracket - 3);
                if (continent.IndexOf('=', endBracket + 1) != endBracket + 1)
                    ThrowException("Error parsing area \"" + areaName + "\"");
                else
                    endBracket++;

                // Create a new area
                GatherItems.Area newArea = new GatherItems.Area(areaName);

                string areaText = GetBlock(continent, endBracket + 1);
                ParseArea(areaText, newArea);
                currentIndex = endBracket + areaText.Length + 3;

                // Eat the comma at the end of each area
                if (continent.IndexOf(',', currentIndex) != currentIndex)
                    ThrowException("Error parsing area \"" + areaName + "\"");
                else
                    currentIndex++;

                // Add the new area to the current continent
                currentContinent.Add(newArea);
            }
        }

        private void ParseArea(string area, GatherItems.Area currentArea)
        {
            int currentIndex = 0;
            while (currentIndex < area.Length)
            {
                // Parse a node's type identifier
                int startBracket = area.IndexOf('[', currentIndex);
                if (startBracket == -1)
                    ThrowException("Error parsing node list");
                int endBracket = area.IndexOf(']', startBracket + 1);

                // We need to handle a special case where power crystals are not represented by an integer ID number
                // but by the special string "TREASURE_POWERCRYST"
                ulong nodeID = 0;
                if (area.IndexOf('\"', startBracket+1) <= endBracket)
                    nodeID = (ulong)GatherItems.Node.GatherType.BluePowerCrystal;
                else
                    nodeID = System.UInt64.Parse(area.Substring(startBracket + 1, endBracket - startBracket - 1));
                if (area.IndexOf('=', endBracket + 1) != endBracket + 1)
                    ThrowException("Error parsing node \"" + nodeID.ToString() + "\"");
                else
                    endBracket++;

                // Create a new node
                GatherItems.Node newNode = new GatherItems.Node(nodeID);
                
                // Parse the locations stored for the node
                string nodeText = GetBlock(area, endBracket + 1);
                ParseNode(nodeText, newNode);
                currentIndex = endBracket + nodeText.Length + 3;

                // Eat the comma at the end of each node
                if (area.IndexOf(',', currentIndex) != currentIndex)
                    ThrowException("Error parsing node \"" + nodeID.ToString() + "\"");
                else
                    currentIndex++;

                // Add the new node to the current area
                currentArea.Add(newNode);
            }
        }

        private void ParseNode(string node, GatherItems.Node currentNode)
        {
            int currentIndex = 0;
            while (currentIndex < node.Length)
            {
                int startBracket = node.IndexOf('[', currentIndex);
                int startBrace = node.IndexOf('{', currentIndex);
                if (startBracket == -1 && startBrace == -1)
                    ThrowException("Error parsing node locations");
                else if (startBrace == -1 || startBracket < startBrace)
                {
                    // Parse node type
                    int endBracket = node.IndexOf(']', startBracket + 1);
                    if (node.Substring(startBracket + 1, endBracket - startBracket - 1) != "\"gtype\"")
                        ThrowException("Error parsing node type");
                    else
                        currentIndex = endBracket + 1;

                    if (node.IndexOf('=', currentIndex) != currentIndex)
                        ThrowException("Error parsing node type");
                    else
                        currentIndex++;

                    // Parse the node type string
                    int startQuotes = node.IndexOf('\"', currentIndex);
                    if (startQuotes == -1)
                        ThrowException("Error parsing node type");
                    int endQuotes = node.IndexOf('\"', startQuotes + 1);
                    string nodeType = (node.Substring(startQuotes + 1, endQuotes - startQuotes - 1));
                    currentIndex = endQuotes + 1;
                    currentNode.typeString = nodeType;

                    // Eat the comma at the end of the node type
                    if (node.IndexOf(',', currentIndex) != currentIndex)
                        ThrowException("Error parsing node type");
                    else
                        currentIndex++;
                }
                else
                {
                    // Parse node location
                    string nodeLocation = GetBlock(node, currentIndex);
                    GatherItems.Location newLocation = ParseNodeLocation(nodeLocation);
                    currentIndex = startBrace + nodeLocation.Length + 2;

                    // Eat the comma at the end of the node location
                    if (node.IndexOf(',', currentIndex) != currentIndex)
                        ThrowException("Error parsing node locations");
                    else
                        currentIndex++;

                    // Eat the comment indicating the node index
                    ParseIndexComment(node, ref currentIndex);

                    // Add the location into the node's list
                    currentNode.Add(newLocation);
                }
            }
        }

        /// <summary>
        /// Parses location data from a correctly formatted string. An exception is thrown if
        /// the string is not correctly formatted.
        /// </summary>
        /// <param name="nodeLocation"></param>
        /// <returns>A location of a gatherable item</returns>
        private GatherItems.Location ParseNodeLocation(string nodeLocation)
        {
            int currentIndex = 0;

            // Parse the x coordinate
            int commaIndex = nodeLocation.IndexOf(',', currentIndex);
            if (commaIndex == -1)
                ThrowException("Error parsing location x coordinate");
            double xPosition = System.Double.Parse(nodeLocation.Substring(currentIndex, commaIndex - currentIndex));
            currentIndex = commaIndex + 1;
            if (ParseIndexComment(nodeLocation, ref currentIndex) != 1)
                ThrowException("Error parsing location x coordinate");

            // Parse the y coordinate
            commaIndex = nodeLocation.IndexOf(',', currentIndex);
            if (commaIndex == -1)
                ThrowException("Error parsing location y coordinate");
            double yPosition = System.Double.Parse(nodeLocation.Substring(currentIndex, commaIndex - currentIndex));
            currentIndex = commaIndex + 1;
            if (ParseIndexComment(nodeLocation, ref currentIndex) != 2)
                ThrowException("Error parsing location y coordinate");

            // Parse the gather count
            commaIndex = nodeLocation.IndexOf(',', currentIndex);
            if (commaIndex == -1)
                ThrowException("Error parsing location gather count");
            long gatherCount = System.Int64.Parse(nodeLocation.Substring(currentIndex, commaIndex - currentIndex));
            currentIndex = commaIndex + 1;
            if (ParseIndexComment(nodeLocation, ref currentIndex) != 3)
                ThrowException("Error parsing location gather count");

            // Parse the harvested time
            commaIndex = nodeLocation.IndexOf(',', currentIndex);
            if (commaIndex == -1)
                ThrowException("Error parsing location harvested time");
            long harvestedTime = System.Int64.Parse(nodeLocation.Substring(currentIndex, commaIndex - currentIndex));
            currentIndex = commaIndex + 1;
            if (ParseIndexComment(nodeLocation, ref currentIndex) != 4)
                ThrowException("Error parsing location harvested time");

            // Parse the inspected time
            commaIndex = nodeLocation.IndexOf(',', currentIndex);
            if (commaIndex == -1)
                ThrowException("Error parsing location inspected time");
            long inspectedTime = System.Int64.Parse(nodeLocation.Substring(currentIndex, commaIndex - currentIndex));
            currentIndex = commaIndex + 1;
            if (ParseIndexComment(nodeLocation, ref currentIndex) != 5)
                ThrowException("Error parsing location inspected time");

            // Create the new location
            GatherItems.Location newLocation = new GatherItems.Location(xPosition, yPosition, gatherCount,
                                                                        harvestedTime, inspectedTime);

            // Parse the source name
            if (currentIndex < nodeLocation.Length)
            {
                commaIndex = nodeLocation.IndexOf(',', currentIndex);
                if (commaIndex == -1)
                    ThrowException("Error parsing location source name");
                string sourceName = nodeLocation.Substring(currentIndex + 1, commaIndex - currentIndex - 2);
                currentIndex = commaIndex + 1;
                if (ParseIndexComment(nodeLocation, ref currentIndex) != 6)
                    ThrowException("Error parsing location source name");

                // Set the location's source name
                newLocation.source = sourceName;
            }
            return newLocation;
        }

        private void ParseGatherDrops(GatherDrops.GatherDropsDB dropsDB, string gatherDrops)
        {
            int currentIndex = 0;

            do
            {
                // find the next node id
                int startingBracket = gatherDrops.IndexOf('[', currentIndex);

                if (startingBracket == -1)
                {
                    break;
                }

                int endingBracket = gatherDrops.IndexOf(']', startingBracket + 1);
                string nodeId = gatherDrops.Substring(startingBracket + 1, endingBracket - startingBracket - 1);

                GatherDrops.Node node = new GatherDrops.Node(nodeId);

                string loots = GetBlock(gatherDrops, endingBracket + 1);

                int currentLootIndex = 0;
                do
                {
                    // find the next loot id and associative count
                    int startingLootBracket = loots.IndexOf('[', currentLootIndex);

                    if (startingLootBracket == -1)
                    {
                        break;
                    }

                    int endingLootBracket = loots.IndexOf(']', startingLootBracket + 1);
                    string lootId = loots.Substring(startingLootBracket + 1, endingLootBracket - startingLootBracket - 1);

                    int commaIndex = loots.IndexOf(',', endingLootBracket);
                    string countString = loots.Substring(endingLootBracket + 2, commaIndex - (endingLootBracket + 2));
                    int count = System.Int32.Parse(countString);

                    GatherDrops.Loot loot = new GatherDrops.Loot(lootId, count);
                    node.AddLoot(loot);

                    currentLootIndex = commaIndex + 1;
                } while (true);

                dropsDB.AddNode(node);

                currentIndex = endingBracket + loots.Length + 1;
            } while (true);
        }

        private void ParseGatherDropRates(GatherDropRates.GatherDropRatesDB dropRatesDB, string gatherDropRates)
        {
            // ensure we are dealing with a database version that we understand
            CheckDatabaseVersion(ref gatherDropRates, "1");

            int currentIndex = 0;

            do
            {
                int startingBrace = gatherDropRates.IndexOf("{", currentIndex);

                if (startingBrace == -1)
                {
                    // no more continents
                    break;
                }

                // get the next continent block that we are dealing with
                string continentBlock = GetBlock(gatherDropRates, startingBrace);

                currentIndex += continentBlock.Length;

                // get the continent id that we are dealing with
                int continentId = ParseIndexComment(gatherDropRates, ref currentIndex);

                // parse the current continent
                ParseContinentDropRates(continentBlock, dropRatesDB[continentId - 1]); // continents ids are 1-based in the lua file
            } while (true);
        }

        private void ParseContinentDropRates(string continentBlock, GatherDropRates.Continent continent)
        {
            int currentIndex = 0;

            do
            {
                // get the name of the next area
                int startingBrace = continentBlock.IndexOf("[", currentIndex);

                if (startingBrace == -1)
                {
                    // no more areas
                    break;
                }

                int endingBrace = continentBlock.IndexOf("]", startingBrace + 1);

                string areaName = continentBlock.Substring(startingBrace + 2, endingBrace - startingBrace - 3);

                GatherDropRates.Area area = new GatherDropRates.Area(areaName);
                continent.AddArea(area);

                currentIndex = endingBrace + 1;

                startingBrace = continentBlock.IndexOf("{", currentIndex);

                // get the next area block that we are dealing with
                string areaBlock = GetBlock(continentBlock, startingBrace);

                currentIndex += areaBlock.Length;

                // parse the current area
                ParseAreaDropRates(areaBlock, area);
            } while (true);
        }

        private void ParseAreaDropRates(string areaBlock, GatherDropRates.Area area)
        {
            int currentIndex = 0;

            do
            {
                // get the type of the next node
                int startingBrace = areaBlock.IndexOf("[", currentIndex);

                if (startingBrace == -1)
                {
                    // no more nodes
                    break;
                }

                int endingBrace = areaBlock.IndexOf("]", startingBrace + 1);

                string nodeType = areaBlock.Substring(startingBrace + 1, endingBrace - startingBrace - 1);

                GatherDropRates.Node node = new GatherDropRates.Node(nodeType);
                area.AddNode(node);

                currentIndex = endingBrace + 1;

                startingBrace = areaBlock.IndexOf("{", currentIndex);

                // get the next area block that we are dealing with
                string nodeBlock = GetBlock(areaBlock, startingBrace);

                currentIndex += nodeBlock.Length;

                // parse the current area
                ParseNodeDropRates(nodeBlock, node);
            } while (true);
        }

        private void ParseNodeDropRates(string nodeBlock, GatherDropRates.Node node)
        {
            int currentIndex = 0;

            do
            {
                // get the type of the next loot
                int startingBrace = nodeBlock.IndexOf("[", currentIndex);

                if (startingBrace == -1)
                {
                    // no more loot
                    break;
                }

                int endingBrace = nodeBlock.IndexOf("]", startingBrace + 1);

                string lootType = nodeBlock.Substring(startingBrace + 1, endingBrace - startingBrace - 1);

                int commaIndex = nodeBlock.IndexOf(',', endingBrace);
                string countString = nodeBlock.Substring(endingBrace + 2, commaIndex - (endingBrace + 2));
                int count = System.Int32.Parse(countString);

                GatherDropRates.Loot loot = new GatherDropRates.Loot(lootType, count);
                node.AddLoot(loot);

                currentIndex = commaIndex + 1;
            } while (true);
        }

        private string GetBlock(string source, int startIndex)
        {
            // find the index of the starting brace
            int startingBrace = source.IndexOf('{', startIndex);
            // TODO handle failure

            int index = startingBrace + 1;
            int depth = 1;

            // keep looking for the ending brace
            // TODO handle end of string before encountering ending brace
            while (depth > 0)
            {
                if (source[index] == '{')
                {
                    ++depth;
                }
                else if (source[index] == '}')
                {
                    --depth;
                }

                ++index;
            }

            // no need to include start and ending brace
            return source.Substring(startingBrace + 1, index - startingBrace - 2);
        }

        private string GetSection(string source, string name)
        {
            // find where the area starts
            int startIndex = source.IndexOf(name);

            if (startIndex == -1)
            {
                return null;
            }

            // check to see if the block is "nil"
            string nil = name + "=nil";

            if (source.IndexOf(nil, startIndex) != -1)
            {
                return null;
            }

            return GetBlock(source, startIndex + 1);
        }

        private void CheckDatabaseVersion(ref string block, string expectedVersion)
        {
            int versionStart = block.IndexOf("[\"dbVersion\"]=");
            int versionStop = block.IndexOf(',', versionStart);
            string dbVersion = block.Substring(versionStart, versionStop - versionStart + 1);

            if (dbVersion != string.Format("[\"dbVersion\"]={0},", expectedVersion))
            {
                ThrowException("Error unknown database format");
            }

            // return the block without the database information
            block = block.Remove(versionStart, versionStop - versionStart + 1);
        }
    }
}
